package org.example.myleet.rosalind.utils;

import org.example.myleet.Utils.Assert;

import java.math.BigDecimal;
import java.math.BigInteger;
import java.math.MathContext;
import java.math.RoundingMode;
import java.util.HashMap;
import java.util.Map;

public class BigNumberOperator {

    public static BigInteger factorial(int n) {
        BigInteger res = BigInteger.ONE;
        for (int i = 2; i <= n; ++i) {
            res = res.multiply(BigInteger.valueOf(i));
        }
        return res;
    }

    public static BigDecimal factorial(BigDecimal p, int pow) {
        BigDecimal result = BigDecimal.ONE;
        for (int i = 0; i < pow; ++i) {
            result = result.multiply(p);
        }
        return result;
    }

    private static Map<String, BigInteger> combinationMemory = new HashMap<>();

    /**
     * pick m from n to form a combination set, no difference for each element, m must be less than n
     */
    public static BigInteger combination(int n, int m) {
        Assert.isTrue(n >= m);
        if (n == m) {
            return BigInteger.ONE;
        }
        if (m == 0) return BigInteger.ONE;
        if (m == 1) {
            return BigInteger.valueOf(n);
        }
        String key = n + "_" + m;
        BigInteger result = combinationMemory.get(key);
        if (null != result) {
            return result;
        }
        BigInteger res = combination(n - 1, m - 1).add(combination(n - 1, m));
        combinationMemory.put(key, res);
        return res;
    }

    /**
     * pick m from n to form a permutation, elements are different from each other, m must be less than n
     */
    public static BigInteger permutation(int n, int m) {
        Assert.isTrue(n >= m);
        BigInteger nFactorial = factorial(n);
        BigInteger notMFactorial = factorial(n - m);
        return nFactorial.divide(notMFactorial);
    }

    public static BigDecimal sqrt(BigDecimal d) {
        return sqrt(d, 10);
    }

    public static BigDecimal sqrt(BigDecimal d, int scale) {
        return sqrt(d, scale, new BigDecimal("0.000001"));
    }

    public static BigDecimal sqrt(BigDecimal d, int scale, BigDecimal upsilon) {
        BigDecimal two = new BigDecimal("2");
        BigDecimal n = new BigDecimal(d.toPlainString());
        BigDecimal next = n.add(d.divide(n, scale, RoundingMode.HALF_UP)).divide(two, scale, RoundingMode.HALF_UP);
        while (n.subtract(next).abs().compareTo(upsilon) > 0) {
            n = new BigDecimal(next.toPlainString());
            next = n.add(d.divide(n, scale, RoundingMode.HALF_UP)).divide(two, scale, RoundingMode.HALF_UP);
        }
        return n;
    }

    //http://everything2.com/index.pl?node_id=946812
    public static BigDecimal log10(BigDecimal b, int dp) {
        final int NUM_OF_DIGITS = dp + 2; // need to add one to get the right number of dp
        //  and then add one again to get the next number
        //  so I can round it correctly.

        MathContext mc = new MathContext(NUM_OF_DIGITS, RoundingMode.HALF_EVEN);

        //special conditions:
        // log(-x) -> exception
        // log(1) == 0 exactly;
        // log of a number lessthan one = -log(1/x)
        if (b.signum() <= 0)
            throw new ArithmeticException("log of a negative number! (or zero)");
        else if (b.compareTo(BigDecimal.ONE) == 0)
            return BigDecimal.ZERO;
        else if (b.compareTo(BigDecimal.ONE) < 0)
            return (log10((BigDecimal.ONE).divide(b, mc), dp)).negate();

        StringBuilder sb = new StringBuilder();
        //number of digits on the left of the decimal point
        int leftDigits = b.precision() - b.scale();

        //so, the first digits of the log10 are:
        sb.append(leftDigits - 1).append(".");

        //this is the algorithm outlined in the webpage
        int n = 0;
        while (n < NUM_OF_DIGITS) {
            b = (b.movePointLeft(leftDigits - 1)).pow(10, mc);
            leftDigits = b.precision() - b.scale();
            sb.append(leftDigits - 1);
            n++;
        }

        BigDecimal ans = new BigDecimal(sb.toString());

        //Round the number to the correct number of decimal places.
        ans = ans.round(new MathContext(ans.precision() - ans.scale() + dp, RoundingMode.HALF_EVEN));
        return ans;
    }
}
